﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace AMOOF {
    

    // runs the integration algorithm
    public class Integrator {

        //public static IntegrationRecord integrate(PDF pdf, double dataMean) {
        //    List<PDF> pdfs = new List<PDF>();
        //    List<double> scale = new List<double>();
        //    scale.Add(1.0);
        //    pdfs.Add(pdf);
        //    MultiplePDF mPDF = new MultiplePDF(pdfs, scale);
        //    return integrate(mPDF, dataMean);
        //}

        //public static IntegrationRecord integrate(MultiplePDF pdfs, double dataMean) {
        //    try {
        //        Integral integral = new Integral(pdfs, dataMean);
        //        IntegrationRecord record = new IntegrationRecord();
        //        record.bounds = integral.bounds;
        //        integral.findTails(record, true);
        //        integral.findTails(record, false);
        //        if (!validIntegration(record)) {
        //            throw new Exception("Failed to find all VAR/CTEs");
        //        }
        //        record.pdf = pdfs;
        //        record.valid = true;
        //        return record;
        //    }
        //    catch (Exception e) {
        //        Program.log(e.Message + "\r\n" + e.StackTrace);
        //        throw new Exception(e.Message);
        //    }
        //}

        public static IntegrationRecord integratePeri(MultiplePDF pdfs) {
            string script = buildPeriScript(pdfs);
            IntegrationRecord record = PeriInterface.integrate(script, pdfs);
            return record;
        }

        private static string buildPeriScript(MultiplePDF pdf) {
            String str = "read(\"" + PeriInterface.periFolderDirectory.Replace('\\', '/') + "/AMOOF_INTEGRATOR.gp\");";
            str += "\r\ntrap(,writeScriptFailed(\"" + PeriInterface.periFolderDirectory.Replace('\\', '/') + "//" + "PERI_CONTROL.txt\"),);";
            str += "\r\n\r\n";
            str += "directory = \"" + PeriInterface.periFolderDirectory.Replace('\\', '/') + "\";";
            str += "\r\n";
            str += pdf.getPeriCommand();
            return str;
        }

        //// test to see if all VARs found.
        //static Boolean validIntegration(IntegrationRecord record) {
        //    double diff = Math.Abs(record.totalArea - 1.0);
        //    if (diff > .001) {
        //        throw new Exception("Total area not equal to 1.0: area:\t" + record.totalArea);
        //    }
        //    return true;
        //}

        // used by Integrator
        public class Integral {
            static MultiplePDF pdf;
            public Boolean CTE;
            public Bounds bounds;
            private static double unitWidth = double.Epsilon * 10000000000000; // this is to prevent bad behavior when numbers get small
            private static double SMALLEST_X = unitWidth;
            private static double LOWEST_Y_ALLOWED = unitWidth;
            private static double LARGEST_X = double.MaxValue * .00000001;
            private static double MAX_Y_ALLOWED = double.MaxValue * .00000001;
            private static double MAX_HORIZONTAL_ERROR = .0000001;
            private static double MAX_INTEGRATION_ERROR = .000001;
            private static double MIN_INTEGRATION_ERROR = MAX_INTEGRATION_ERROR / 100;
            private static double MAX_TIME_ALLOWED = 20000;
            private static double MAX_LEVEL_ALLOWED = 600;
            private static Stopwatch stopWatch = new Stopwatch();
            private static double SMALLEST_BAR_WIDTH = unitWidth * 1000000;

            // constructor
            public Integral(MultiplePDF pf, double dataMn) {
                pdf = pf;
                for (int i = 0; i < pf.pdfs.Count; i++) {
                    PDF singlePdf = pf.pdfs.ElementAt(i);
                    Bounds tempBounds = findPDFBounds(singlePdf, dataMn, 2);
                    pdf.bounds.Add(tempBounds);
                    pdf.highPoints.Add(findHighPoint(tempBounds.left, tempBounds.right, singlePdf));
                }
            }

            static int currentIndex;
            static IntegrationRecord record;
            static int count2 = 0;
            static VarCtePair currentCTE;
            static int PARTITIONS = 20;
            static Boolean movingLeft;
            static double maxSegmentSize;
            static double minSegmentSize;
            static double desiredArea;

            // find the highpoint of the PDF using binary search (all are assumed to be unimodal)
            double findHighPoint(double left, double right, PDF pdf) {
                Boolean done = false;
                double partitions = 10;
                while (!done) {
                    if (stopWatch.ElapsedMilliseconds > MAX_TIME_ALLOWED) {
                        throw new Exception("FindHighPoint: timeout");
                    }
                    double partitionWidth = Math.Abs(left - right) / partitions;
                    int bestIndex = 0;
                    double localHighpoint = double.MinValue;
                    for (int i = 0; i < partitions; i++) {
                        double localLeft = left + partitionWidth * i;
                        double localRight = localLeft + partitionWidth;
                        double localCenter = (localLeft + localRight) / 2;
                        double output = pdf.getOutput(localCenter);
                        if (output > localHighpoint) {
                            localHighpoint = output;
                            bestIndex = i;
                        }
                    }
                    int leftIndex = Math.Max(bestIndex - 1, 0);
                    int rightIndex = Math.Min(bestIndex + 1, 0);
                    left = partitionWidth * leftIndex;
                    right = partitionWidth * rightIndex + partitionWidth;
                    if (Math.Abs(left - right) < .0000001) {
                        done = true;
                    }
                }
                return (left + right) / 2;
            }

            // accumulates area
            public class Area {
                public double areaNormal = 0;
                public double areaX = 0;
                public Area add(Area other) {
                    areaNormal += other.areaNormal;
                    areaX += other.areaX;
                    return this;
                }
            }

            public static void adjustMaxSegmentSize(double left, double right, Boolean movingLeft) {
                double distance = movingLeft ? pdf.getDistanceFromHighpoint(right) : pdf.getDistanceFromHighpoint(left);
                maxSegmentSize = distance / 10 + .1;
                minSegmentSize = maxSegmentSize * .00001;
                minSegmentSize = Math.Max(minSegmentSize, SMALLEST_BAR_WIDTH);
            }

            // object used to recursively find area
            public class Bucket {
                private double width(double start, double end) {
                    return Math.Abs(start - end);
                }
                public int level;
                private double left;
                private double right;

                public Bucket(double childLeft, double childRight, int lev) {
                    this.left = childLeft;
                    this.right = childRight;
                    level = lev;
                }

                // calculate the area corresponding to the bucket's boundaries
                public void addArea(
                    Area accumulator) {
                    if (level > MAX_LEVEL_ALLOWED) {
                        throw new Exception("Exceeded max level");
                    }
                    if (stopWatch.ElapsedMilliseconds > MAX_TIME_ALLOWED) {
                        throw new Exception("Bucket: addArea: timeout");
                    }

                    if (currentIndex == 0 || currentIndex == 100) {
                        return;
                    }

                    // find this buckets stats
                    SegmentArea segment = Integrator.Integral.getSegmentArea(left, right, pdf);
                    double addedArea = accumulator.areaNormal + segment.areaNormal;
                    double integrationError = segment.errorNormal;
                    double horizontalError = 0;
                    if (addedArea > desiredArea) {
                        horizontalError = Math.Abs(addedArea - desiredArea); // horizontal error is the area past the desired area for the current VAR
                    }
                    adjustMaxSegmentSize(left, right, movingLeft);
                    Boolean intError = segment.errorNormal > MAX_INTEGRATION_ERROR; // integration error
                    Boolean horizError = horizontalError > MAX_HORIZONTAL_ERROR && addedArea > desiredArea;
                    double segWidth = Math.Abs(left - right);
                    Boolean segmentTooWide = segWidth > maxSegmentSize;
                    Boolean canDivide = true;
                    canDivide = segWidth > minSegmentSize; // if allowed to get too small, causes roundoff errors, especially with gaussian calculation

                    double segmentWidth = width(left, right) / PARTITIONS;
                    for (int i = 0; i < PARTITIONS; i++) {
                        double chldlft = left + i * segmentWidth;
                        double chldrht = chldlft + segmentWidth;
                        if (chldlft == chldrht) {
                            canDivide = false;
                        }
                    }

                    // split bucket if too much error
                    if ((intError || horizError || segmentTooWide) && canDivide) {
                        SegmentArea segment2 = Integrator.Integral.getSegmentArea(left, right, pdf);
                        int startI = movingLeft ? PARTITIONS - 1 : 0;
                        int endI = movingLeft ? -1 : PARTITIONS;
                        while (startI != endI) {
                            double childLeft = left + startI * segmentWidth;
                            double childRight = childLeft + segmentWidth;
                            Bucket childBucket = new Bucket(childLeft, childRight, level + 1);
                            childBucket.addArea(accumulator);
                            startI = movingLeft ? startI - 1 : startI + 1;
                        }
                        return;
                    }

                    // add area if leaf node
                    accumulator.areaNormal += segment.areaNormal;
                    accumulator.areaX += segment.areaMultiplyByX;
                    count2++;
                    if (count2 % 10000 == 0) {
                        //Console.Clear();
                        Console.WriteLine("Lowest Level: " +
                            "Area: " + accumulator.areaNormal + "".PadRight(5) +
                            "\nLevel: ".PadRight(15) + level +
                            "\nLeft: ".PadRight(15) + left +
                            "\nRight: ".PadRight(15) + right +
                            "\ncount: ".PadRight(15) + count2 +
                            "\nmaxwidth".PadRight(15) + maxSegmentSize +
                            "\ncurrentWidth".PadRight(15) + Math.Abs(left - right) +
                            "\nmoving left:".PadRight(15) + movingLeft +
                            "\nTotal Area:".PadRight(15) + accumulator.areaNormal +
                            "\n\nVARCTE:".PadRight(15) + currentCTE.ToString() +
                            "\nmoving left:".PadRight(15) + movingLeft +
                            "\noutput:".PadRight(15) + pdf.getOutput(left) +
                            "\n\n");
                    }

                    // record VARCTE's when appropriate
                    if (accumulator.areaNormal > desiredArea) {
                        if (movingLeft) {
                            currentCTE.rightCTEFound = true;
                            currentCTE.var = left;
                            currentCTE.rightCte = accumulator.areaX / accumulator.areaNormal;
                            currentIndex--;
                            currentCTE = record.tails.ElementAt(currentIndex);
                            desiredArea =
                        movingLeft ? 1 - currentCTE.leftPercentile / 100.0 :
                        currentCTE.leftPercentile / 100.0;
                        }
                        else {
                            currentCTE.leftCTEFound = true;
                            currentCTE.var = right;
                            currentCTE.integral = accumulator.areaNormal;
                            currentCTE.leftCte = accumulator.areaX / accumulator.areaNormal;
                            currentIndex++;
                            currentCTE = record.tails.ElementAt(currentIndex);
                            desiredArea =
                        movingLeft ? 1 - currentCTE.leftPercentile / 100.0 :
                        currentCTE.leftPercentile / 100.0;
                        }
                    }
                }
            }

            // find tails
            public void findTails(IntegrationRecord rec, Boolean movingLft) {
                stopWatch.Reset();
                stopWatch.Start();
                QuadWeights.initialize();
                Console.WriteLine("===============");
                List<Bounds> boundsList = pdf.getBounds();
                movingLeft = movingLft;
                currentIndex = movingLeft ? 99 : 1;
                record = rec;
                currentCTE = record.tails.ElementAt(currentIndex);
                desiredArea =
                        movingLeft ? 1 - currentCTE.leftPercentile / 100.0 :
                        currentCTE.leftPercentile / 100.0;
                int currentBoundsIndex = movingLeft ? boundsList.Count - 1 : 0;
                int stopBoundsIndex = movingLeft ? -1 : boundsList.Count;
                Area accumulator = new Area();
                while (currentBoundsIndex != stopBoundsIndex) { // with multiple PDFs, there may be several boundaries
                    bounds = boundsList.ElementAt(currentBoundsIndex);
                    Bucket b = new Bucket(bounds.left, bounds.right, 0);
                    b.addArea(accumulator);
                    currentBoundsIndex = movingLeft ? currentBoundsIndex - 1 : currentBoundsIndex + 1;
                }
            }

            // holds information about area of a segment
            public class SegmentArea {
                public Bounds bounds;
                public double areaNormal;
                public double errorNormal;
                public double areaMultiplyByX; // multiplyByX is required for CTE integral (numerator = x * f(x))
                public double errorMultiplyByX;
                public double error() {
                    return Math.Max(errorNormal, errorMultiplyByX);
                }
            }

            // wrapper method
            public static double getOutput(PDF pdf, double x, Boolean multiplyByX) {
                if (multiplyByX) {
                    return pdf.getOutput(x) * x;
                }
                return pdf.getOutput(x);
            }

            public static class QuadWeights {
                public static List<WeightNodePair> weightNodes;
                public static void initialize() {
                    weightNodes = new List<WeightNodePair>();
                    weightNodes.Add(new WeightNodePair() { node = 0.1488743389816312108848260, weight = 0.2955242247147528701738930 });
                    weightNodes.Add(new WeightNodePair() { node = -0.1488743389816312108848260, weight = 0.2955242247147528701738930 });
                    weightNodes.Add(new WeightNodePair() { node = 0.4333953941292471907992659, weight = 0.2692667193099963550912269 });
                    weightNodes.Add(new WeightNodePair() { node = -0.4333953941292471907992659, weight = 0.2692667193099963550912269 });
                    weightNodes.Add(new WeightNodePair() { node = 0.6794095682990244062343274, weight = 0.2190863625159820439955349 });
                    weightNodes.Add(new WeightNodePair() { node = -0.6794095682990244062343274, weight = 0.2190863625159820439955349 });
                    weightNodes.Add(new WeightNodePair() { node = 0.8650633666889845107320967, weight = 0.1494513491505805931457763 });
                    weightNodes.Add(new WeightNodePair() { node = -0.8650633666889845107320967, weight = 0.1494513491505805931457763 });
                    weightNodes.Add(new WeightNodePair() { node = 0.9739065285171717200779640, weight = 0.0666713443086881375935688 });
                    weightNodes.Add(new WeightNodePair() { node = -0.9739065285171717200779640, weight = 0.0666713443086881375935688 });
                }
            }
            public class WeightNodePair {
                public double weight;
                public double node;
            }

            public static double gaussianQuadrature(PDF pdf, double a, double b, Boolean multiplyByX) {
                // http://www.holoborodko.com/pavel/numerical-methods/numerical-integration/
                // http://autarkaw.wordpress.com/2009/02/28/an-efficient-formula-for-an-automatic-integrator-based-on-trapezoidal-rule/

                double sum = 0;
                foreach (WeightNodePair pair in QuadWeights.weightNodes) {

                    double f1 = pair.weight * getOutput(pdf, (b - a) / 2 * pair.node + (a + b) / 2, multiplyByX);
                    sum += f1;
                }
                double distance = b - a;
                if ((b - a) == 0) {
                    int s = 0;
                }
                double output = (distance) / 2.0 * sum;
                return output;
            }

            // returns information about area of a segment
            public static SegmentArea getSegmentArea(double a, double b, PDF pdf) {
                SegmentArea segmentArea = new SegmentArea();
                segmentArea.bounds = new Bounds() { left = a, right = b };
                double distanceX = b - a;
                double integralNormal = gaussianQuadrature(pdf, a, b, false);
                double integralX = gaussianQuadrature(pdf, a, b, true);
                segmentArea.areaMultiplyByX = integralX;
                segmentArea.areaNormal = integralNormal;
                segmentArea.errorNormal = getError(pdf, a, b);
                if (segmentArea.errorNormal == 1.0) {
                    getError(pdf, a, b);
                }
                return segmentArea;
            }
            public static Boolean xValid(double x, PDF pdf) {
                if (x > LARGEST_X || x < double.Epsilon) {
                    return false;
                }
                try {
                    if (!numValid(getOutput(pdf, x, false))) {
                        return false;
                    }
                    if (!numValid(getOutput(pdf, x, true))) {
                        return false;
                    }
                }
                catch (Exception e) {
                    Program.log(e.Message + "\r\n" + e.StackTrace);
                    return false;
                }
                return true;
            }

            public static Boolean numValid(double num) {
                if (double.IsNaN(num) ||
                    double.IsPositiveInfinity(num) ||
                    double.IsNegativeInfinity(num) ||
                    num > MAX_Y_ALLOWED ||
                    num < LOWEST_Y_ALLOWED ||
                    num == 0) {
                    return false;
                }
                return true;
            }

            // estimated error
            public static double getError(PDF pdf, double start, double end) {
                double partitions = 4;
                double width = Math.Abs(start - end);
                double partitionWidth = width / partitions;
                double coarseArea = gaussianQuadrature(pdf, start, end, false);
                double fineArea = 0;

                for (int i = 0; i < partitions; i++) {
                    double thisStart = start + i * partitionWidth;
                    double thisEnd = thisStart + partitionWidth;
                    double thisArea = gaussianQuadrature(pdf, thisStart, thisEnd, false);
                    fineArea += thisArea;
                }
                if (coarseArea == 0 || fineArea == 0) {
                    return 0;
                }
                double areaDist = Math.Abs(fineArea - coarseArea);
                if (areaDist < .000000000000000001) // very small numbers cause invalid error estimates
                {
                    return 0;
                }
                double error = areaDist / coarseArea;
                return error;
            }

            // once the region is found that contains the boundary, make more precise
            public static double binarySearchFindBounds(Bounds containsEdge, PDF pdf, Boolean leftSearch) {

                Boolean leftValid = xValid(containsEdge.left, pdf);
                Boolean rightValid = xValid(containsEdge.right, pdf);
                int count2 = 0;

                if (leftValid && rightValid) {
                    throw new Exception("Integrator: binary search: whileloop: neither edges valid");
                }
                if (!leftValid && !rightValid) {
                    throw new Exception("Integrator: binary search: whileloop: nothing in bounds valid");
                }
                double widthEdge = containsEdge.getWidth();
                double targetWidth = unitWidth;
                Boolean done = false;
                while (!done) {
                    count2++;
                    if (count2 % 1000 == 0) {
                        double midpoint = (containsEdge.left + containsEdge.right) / 2;
                        Console.WriteLine("\n=============BinarySearchBounds\n" +
                            "output: ".PadRight(10) + pdf.getOutput(midpoint) +
                            "midpoint: ".PadRight(10) + midpoint
                            );
                    }
                    double width = containsEdge.getWidth();


                    leftValid = xValid(containsEdge.left, pdf);
                    rightValid = xValid(containsEdge.right, pdf);
                    if (leftValid && rightValid) {
                        throw new Exception("Integrator: binary search: whileloop: both edges valid");
                    }
                    if (!leftValid && !rightValid) {
                        throw new Exception("Integrator: binary search: whileloop: nothing in bounds valid");
                    }
                    double center = containsEdge.getCenter();
                    if (containsEdge.left == center || containsEdge.right == center) {
                        done = true;
                    }
                    if (center <= 0) {
                        containsEdge.getCenter();
                    }
                    Boolean centerValid = xValid(center, pdf);
                    if (stopWatch.ElapsedMilliseconds > MAX_TIME_ALLOWED) {
                        throw new Exception("binarySearchFindBounds: timeout");
                    }
                    if (leftSearch) {
                        if (!centerValid) {
                            containsEdge.left = center;
                        }
                        else {
                            containsEdge.right = center;
                        }
                    }
                    else {
                        if (!centerValid) {
                            containsEdge.right = center;
                        }
                        else {
                            containsEdge.left = center;
                        }
                    }
                    if (containsEdge.getWidth() < targetWidth) {
                        done = true;
                    }
                }
                if (leftSearch) {
                    return containsEdge.right;
                }
                else {
                    return containsEdge.left;
                }
            }

            // looks for where the pdf bounds exist
            public static Bounds findPDFBounds(PDF pdf, double dataMean, double changeProportion) {
                QuadWeights.initialize();
                stopWatch.Reset();
                stopWatch.Start();
                Console.WriteLine("\n\n=============FINDING BOUNDS"); // debug
                Bounds finalBounds = new Bounds();

                double scaleFactor = 1 + changeProportion;
                double delta = unitWidth * 10000;
                double leftBracket = 0;
                double rightBracket;
                Boolean done = false;

                int count = 0;
                double deltaStore = delta;
                Console.WriteLine("Finding initial x");
                while (!done) {
                    count++;
                    if (count % 100000 == 0) {
                        //Console.Clear();
                        Console.WriteLine("\n=============find initial x\n" +
                            "output: ".PadRight(10) + pdf.getOutput(leftBracket) +
                            "\nleftBracket: ".PadRight(10) + leftBracket +
                            "\nChange Proportion: ".PadRight(10) + changeProportion
                            );
                    }
                    if (stopWatch.ElapsedMilliseconds > MAX_TIME_ALLOWED) {
                        throw new Exception("findPDFBounds, find valid left: timeout");
                    }
                    if (xValid(leftBracket, pdf)) {
                        done = true;
                        continue;
                    }

                    leftBracket += delta;
                    deltaStore = delta;
                    delta *= scaleFactor;
                    if (delta == deltaStore) {
                    }
                    if (leftBracket > LARGEST_X) {
                        return (findPDFBounds(pdf, dataMean, changeProportion * .5));
                    }
                }

                // find left 
                done = false;
                rightBracket = leftBracket;
                leftBracket -= delta;
                if (leftBracket <= 0) {
                    int s = 0;
                }
                Bounds containsEdge = new Bounds() { left = leftBracket, right = rightBracket };
                Console.WriteLine("beginning binary search for left bracket");
                finalBounds.left = binarySearchFindBounds(containsEdge, pdf, true);
                Console.WriteLine("left found");

                // find right
                done = false;
                leftBracket = rightBracket;
                delta = unitWidth;
                containsEdge = null;
                Console.WriteLine("finding right bound");
                while (!done) {
                    count++;
                    if (count % 100000 == 0) {
                        //Console.Clear();
                        Console.WriteLine("\n=============find bounds right\n" +
                            "\noutput: ".PadRight(10) + pdf.getOutput(rightBracket) +
                            "\nrightBracket: ".PadRight(10) + rightBracket
                            );
                    }

                    if (stopWatch.ElapsedMilliseconds > MAX_TIME_ALLOWED) {
                        throw new Exception("findBounds: timeout");
                    }
                    if (!xValid(rightBracket, pdf)) {
                        done = true;
                        Console.Write("beginning binary search right bound");
                        containsEdge = new Bounds() { left = leftBracket, right = rightBracket };
                        finalBounds.right = binarySearchFindBounds(containsEdge, pdf, false);
                        continue;
                    }
                    leftBracket = rightBracket;
                    rightBracket += delta;
                    delta *= scaleFactor;
                    if (leftBracket > LARGEST_X) {
                        return (findPDFBounds(pdf, dataMean, changeProportion * .5));
                    }
                }

                Console.WriteLine("Right found");

                double startVal = pdf.getOutput(finalBounds.left);
                double endVal = pdf.getOutput(finalBounds.right);
                return finalBounds;
            }
        }
    }

    // holds information for a single VAR, and corresponding CTE
    public class VarCtePair {
        public int leftPercentile;
        public double var;
        public double leftCte;
        public double rightCte;
        public double integral;
        public Boolean leftCTEFound = false;
        public Boolean rightCTEFound = false;
        public override string ToString() {
            String s = "";
            s += "Percentile: " + leftPercentile +
                "VAR: " + var +
                "\tCTE: " + leftCte +
                "\tIntegral: " + integral;
            return s;
        }
    }

    // holds information about integration of a specific PDF
    public class IntegrationRecord {
        public PDF pdf;
        public List<VarCtePair> tails;
        public Bounds bounds;
        public Boolean valid;
        public double xMean;
        public double totalArea { get; set; }
        public string boundsString = "";

        public IntegrationRecord() {
            tails = new List<VarCtePair>();
            for (int i = 0; i <= 100; i++) {
                tails.Add(new VarCtePair() { leftPercentile = i });
            }
        }

        public string ToString() {
            string returnS = "";
            for (int i = 1; i < 100; i++) {
                VarCtePair matlabPair = getPercentile(i);
                double matlabCTE = matlabPair.leftCte;
                double matlabRightCTE = matlabPair.rightCte;
                double matlabPercentile = matlabPair.leftPercentile;
                double matlabVAR = matlabPair.var;
                string result = "\tMatlab\t\tIntegrator\n" +
                    "Area at this point: " + matlabPair.integral +
                    "\nPercentile: " + matlabPercentile + "\t" +
                    "\nVAR: " + matlabVAR +
                "\nLCTE: " + matlabCTE +
                "\nRCTE: " + matlabRightCTE +
                "\n\n";
                returnS += result;
            }
            return returnS;
        }

        public VarCtePair getPercentile(int percentile) {
            if (percentile < 1 || percentile > 99) {
                throw new Exception("IntegrationRecord: getPercentile: invalid percentile");
            }

            return tails.Single(s => s.leftPercentile == percentile);
        }
    }

    // specifies a segment on the x-axis
    public class Bounds {
        public double left;
        public double right;
        public double getWidth() {
            return Math.Abs(right - left);
        }

        public string getString() {
            return "[" + left + "".PadRight(5) + "\t" + right + "]";
        }
        public double getCenter() {
            double center = (right + left) / 2;
            return (right + left) / 2;
        }
        public Boolean containsValue(double x) {
            if (left < x && right > x) {
                return true;
            }
            return false;
        }
        public static Boolean withinRange(double low, double x, double high) {
            if (low < x && high > x) {
                return true;
            }
            return false;
        }
    }
}
